<?php

namespace App\Library\Lock;

use App\Service\BaseService;
use Closure;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;

/**
 * 单线程锁
 */
class NxLock
{
    /**
     * 等待重试的秒数
     * @var float
     */
    private float $retrySleepSeconds = 0.1;

    /**
     * 死锁自动释放秒数
     * @var int
     */
    private int $deadLockSeconds = 5;

    private static ?int $retryNum = null;

    public function __construct()
    {
        if (app()->runningInConsole()) {
            self::$retryNum = null;
        } else {
            self::$retryNum = 0;
        }
    }

    /**
     * 抢锁
     * @param Closure $closureDo 抢到锁后执行的闭包
     * @param Closure|null $closureReTry 未抢到锁后执行的闭包
     * @param string $lockKey
     * @return mixed
     */
    public function nx(Closure $closureDo, ?Closure $closureReTry, string $lockKey)
    {
        // 抢到锁
        if (Redis::setnx($lockKey, time())) {
            try {
                $res = $closureDo();
            } finally {
                Redis::del($lockKey);
            }

            return $res;
        }

        // 没抢到锁
        $lockTime = Redis::get($lockKey);

        if ($lockTime && time() - $lockTime > $this->deadLockSeconds) {

            // 超过了死锁时间，自动释放锁
            Redis::del($lockKey);
        }

        // 记录重试次数
        $this->incrRetryNum();

        // 重试次数过多，抛出异常
        if (self::$retryNum > 10) {
            BaseService::throwErr('目前请求的人数过多，请稍后再试~');
        }

        // 不需要重试
        if ($closureReTry === null) {
            return null;
        }

        // 等待重试
        $t = (int)ceil($this->retrySleepSeconds * 1000000);
        usleep($t);

        Log::info("NxLock_nx_retry {$lockKey} retryNum:" . self::$retryNum);

        return $closureReTry();
    }

    public function setDeadLockSeconds(int $seconds): self
    {
        $this->deadLockSeconds = $seconds;

        return $this;
    }

    public function setRetrySleepSeconds(float $seconds): self
    {
        $this->retrySleepSeconds = $seconds;

        return $this;
    }

    /**
     * 生成LockKey
     * @param string $prefix
     * @param ...$args
     * @return string
     */
    public static function generateLockKey(string $prefix, ...$args): string
    {
        return $prefix . ':' . implode(':', $args);
    }

    private function incrRetryNum()
    {
        if (self::$retryNum === null) {
            return;
        }

        self::$retryNum++;
    }
}
